/*	Copyright 2013 RangerUFO
 *
 *	This file is part of Libevent-helper.
 *
 *	Libevent-helper is free software: you can redistribute it and/or modify
 *	it under the terms of the GNU Lesser General Public License as published by
 *	the Free Software Foundation, either version 3 of the License, or
 *	(at your option) any later version.
 *
 *	Libevent-helper is distributed in the hope that it will be useful,
 *	but WITHOUT ANY WARRANTY; without even the implied warranty of
 *	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *	GNU Lesser General Public License for more details.
 *
 *	You should have received a copy of the GNU Lesser General Public License
 *	along with Libevent-helper.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "event2/session.h"
#include "event2/helper.h"
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>

struct evsession_filter
{
	evsession_filter_cb input_cb;
	evsession_filter_cb output_cb;
	void* args;
};

struct evsession
{
	struct bufferevent* backend;
	struct bufferevent* frontend;
	evsession_data_cb read_cb;
	evsession_data_cb write_cb;
	evsession_event_cb event_cb;
	void* args;
};

static void bev_read_cb(struct bufferevent* bev, void* args)
{
	struct evsession* s = (struct evsession*)args;

	if (s->read_cb)
	{
		s->read_cb(s, s->args);
	}
}

static void bev_write_cb(struct bufferevent* bev, void* args)
{
	struct evsession* s = (struct evsession*)args;

	if (s->write_cb)
	{
		s->write_cb(s, s->args);
	}
}

static void bev_event_cb(struct bufferevent* bev, short what, void* args)
{
	struct evsession* s = (struct evsession*)args;

	if (s->event_cb)
	{
		s->event_cb(s, what, s->args);
	}
}

static enum bufferevent_filter_result bev_filter_input_cb(struct evbuffer* src, struct evbuffer* dst, ev_ssize_t dst_limit, enum bufferevent_flush_mode mode, void* args)
{
	struct evsession_filter* f = (struct evsession_filter*)args;

	if (f->input_cb)
	{
		return f->input_cb(src, dst, f->args);
	}

	if (evbuffer_add_buffer(dst, src) == 0)
	{
		return BEV_OK;
	}

	return BEV_ERROR;
}

static enum bufferevent_filter_result bev_filter_output_cb(struct evbuffer* src, struct evbuffer* dst, ev_ssize_t dst_limit, enum bufferevent_flush_mode mode, void* args)
{
	struct evsession_filter* f = (struct evsession_filter*)args;

	if (f->output_cb)
	{
		return f->output_cb(src, dst, f->args);
	}

	if (evbuffer_add_buffer(dst, src) == 0)
	{
		return BEV_OK;
	}

	return BEV_ERROR;
}

struct evsession* evsession_new(struct event_base* base, evutil_socket_t fd)
{
	if (!base)
	{
		return 0;
	}

	struct bufferevent* bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_THREADSAFE | BEV_OPT_UNLOCK_CALLBACKS | BEV_OPT_DEFER_CALLBACKS);

	if (!bev)
	{
		return 0;
	}

	if (bufferevent_enable(bev, EV_READ | EV_WRITE) == -1)
	{
		bufferevent_free(bev);
		return 0;
	}

	struct evsession* s = (struct evsession*)evhelper_malloc(sizeof(struct evsession));

	if (!s)
	{
		bufferevent_free(bev);
		return 0;
	}

	memset(s, 0, sizeof(struct evsession));
	s->backend = bev;
	s->frontend = bev;

	bufferevent_setcb(bev, bev_read_cb, bev_write_cb, bev_event_cb, s);

	return s;
}

void evsession_free(struct evsession* s)
{
	if (!s)
	{
		return;
	}

	if (s->frontend != s->backend)
	{
		bufferevent_free(s->frontend);
	}

	bufferevent_free(s->backend);

	evhelper_free(s);
}

struct event_base* evsession_get_base(struct evsession* s)
{
	if (!s)
	{
		return 0;
	}

	return bufferevent_get_base(s->backend);
}

void evsession_setcb(
		struct evsession* s,
		evsession_data_cb read_cb,
		evsession_data_cb write_cb,
		evsession_event_cb event_cb,
		void* args)
{
	if (s)
	{
		s->read_cb = read_cb;
		s->write_cb = write_cb;
		s->event_cb = event_cb;
		s->args = args;
	}
}

int evsession_add_filter(
		struct evsession* s,
		evsession_filter_cb input_cb,
		evsession_filter_cb output_cb,
		void* args)
{
	if (!s)
	{
		return -1;
	}

	struct evsession_filter* f = (struct evsession_filter*)evhelper_malloc(sizeof(struct evsession_filter));

	if (!f)
	{
		return -1;
	}

	memset(f, 0, sizeof(struct evsession_filter));
	f->input_cb = input_cb;
	f->output_cb = output_cb;
	f->args = args;

	int opt = BEV_OPT_THREADSAFE | BEV_OPT_UNLOCK_CALLBACKS | BEV_OPT_DEFER_CALLBACKS;

	if (s->frontend != s->backend)
	{
		opt |= BEV_OPT_CLOSE_ON_FREE;
	}

	struct bufferevent* bev = bufferevent_filter_new(s->frontend, bev_filter_input_cb, bev_filter_output_cb, opt, evhelper_free, f);

	if (!bev)
	{
		evhelper_free(f);
		return -1;
	}

	if (bufferevent_enable(bev, EV_READ | EV_WRITE) == -1)
	{
		bufferevent_free(bev);
		return -1;
	}

	s->frontend = bev;

	bufferevent_setcb(bev, bev_read_cb, bev_write_cb, bev_event_cb, s);

	return 0;
}

void evsession_clear_filters(struct evsession* s)
{
	if (s && s->frontend != s->backend)
	{
		bufferevent_free(s->frontend);
		s->frontend = s->backend;
	}
}

int evsession_connect(struct evsession* s, const char* ip, int port)
{
	if (!s)
	{
		return -1;
	}

	struct sockaddr_in sin;

	memset(&sin, 0, sizeof(sin));
	sin.sin_family = AF_INET;
	sin.sin_addr.s_addr = inet_addr(ip);
	sin.sin_port = htons(port);

	return bufferevent_socket_connect(s->backend, (struct sockaddr*)&sin, sizeof(sin));
}

int evsession_connect_hostname(struct evsession* s, const char* host, int port)
{
	if (!s)
	{
		return -1;
	}

	return bufferevent_socket_connect_hostname(s->backend, 0, AF_UNSPEC, host, port);
}

int evsession_get_dns_error(struct evsession* s)
{
	if (!s)
	{
		return 0;
	}

	return bufferevent_socket_get_dns_error(s->backend);
}

int evsession_write(struct evsession* s, const void* data, size_t len)
{
	if (!s)
	{
		return -1;
	}

	return bufferevent_write(s->frontend, data, len);
}

int evsession_write_buffer(struct evsession* s, struct evbuffer* buf)
{
	if (!s)
	{
		return -1;
	}

	return bufferevent_write_buffer(s->frontend, buf);
}

size_t evsession_read(struct evsession* s, void* data, size_t len)
{
	if (!s)
	{
		return 0;
	}

	return bufferevent_read(s->frontend, data, len);
}

int evsession_read_buffer(struct evsession* s, struct evbuffer* buf)
{
	if (!s)
	{
		return -1;
	}

	return bufferevent_read_buffer(s->frontend, buf);
}

struct evbuffer* evsession_get_input(struct evsession* s)
{
	if (!s)
	{
		return 0;
	}

	return bufferevent_get_input(s->frontend);
}

struct evbuffer* evsession_get_output(struct evsession* s)
{
	if (!s)
	{
		return 0;
	}

	return bufferevent_get_output(s->frontend);
}

int evsession_enable(struct evsession* s, short ev)
{
	if (!s)
	{
		return -1;
	}

	return bufferevent_enable(s->backend, ev);
}

int evsession_disable(struct evsession* s, short ev)
{
	if (!s)
	{
		return -1;
	}

	return bufferevent_disable(s->backend, ev);
}

short evsession_get_enabled(struct evsession* s)
{
	if (!s)
	{
		return 0;
	}

	return bufferevent_get_enabled(s->backend);
}

int evsession_set_timeouts(struct evsession* s, const struct timeval* tr, const struct timeval* tw)
{
	if (!s)
	{
		return -1;
	}

	return bufferevent_set_timeouts(s->backend, tr, tw);
}

int evsession_set_rate_limit(struct evsession* s, struct ev_token_bucket_cfg* cfg)
{
	if (!s)
	{
		return -1;
	}

	return bufferevent_set_rate_limit(s->backend, cfg);
}

int evsession_add_to_rate_limit_group(struct evsession* s, struct bufferevent_rate_limit_group* g)
{
	if (!s)
	{
		return -1;
	}

	return bufferevent_add_to_rate_limit_group(s->backend, g);
}

int evsession_remove_from_rate_limit_group(struct evsession* s)
{
	if (!s)
	{
		return -1;
	}

	return bufferevent_remove_from_rate_limit_group(s->backend);
}

size_t evsession_get_read_limit(struct evsession* s)
{
	if (!s)
	{
		return 0;
	}

	return bufferevent_get_read_limit(s->backend);
}

size_t evsession_get_write_limit(struct evsession* s)
{
	if (!s)
	{
		return 0;
	}

	return bufferevent_get_write_limit(s->backend);
}

const char* evsession_get_peer_ip(struct evsession* s)
{
	if (!s)
	{
		return 0;
	}

	struct sockaddr_in sin;
	socklen_t len = sizeof(sin);

	getpeername(bufferevent_getfd(s->backend), (struct sockaddr*)&sin, &len);

	return inet_ntoa(sin.sin_addr);
}

int evsession_get_peer_port(struct evsession* s)
{
	if (!s)
	{
		return 0;
	}

	struct sockaddr_in sin;
	socklen_t len = sizeof(sin);

	getpeername(bufferevent_getfd(s->backend), (struct sockaddr*)&sin, &len);

	return ntohs(sin.sin_port);
}
